package com.ryan.mdp.advice.reference;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.ryan.mdp.advice.AdviceLifecycle;
import com.ryan.mdp.annotation.parser.MdpMethodConfig;
import com.ryan.mdp.annotation.parser.MdpMethodParser;
import com.ryan.mdp.annotation.parser.MdpServiceConfig;

public class MdpCacheAdvice implements MethodInterceptor, AdviceLifecycle {
    private final Logger logger = LoggerFactory.getLogger(MdpCacheAdvice.class);
    /**
     * 清除过期缓存频率，单位：秒
     */
    private static final long CLEAR_EXPIRED_RATE = 300;
    private final Map<CacheKey, CacheValue> cache = new ConcurrentHashMap<CacheKey, CacheValue>();
    private ScheduledExecutorService scheduler = null;

    public Object invoke(MethodInvocation invocation) throws Throwable {
        Class<?> mdpInterface = invocation.getMethod().getDeclaringClass();
        MdpServiceConfig mdpServiceConfig = MdpMethodParser.getMdpServiceConfig(mdpInterface);
        MdpMethodConfig mdpMethodConfig = mdpServiceConfig.get(invocation.getMethod());
        Object result = null;
        if (mdpMethodConfig.isCache()) {
            result = get(invocation);
            if (result != null) {
                return result;
            }
        }
        result = invocation.proceed();
        if (mdpMethodConfig.isCache()) {
            set(invocation, result, mdpMethodConfig.getCacheTtl());
        }
        return result;
    }

    /**
     * 从缓存中获取数据
     * 
     * @param invocation
     * @return
     */
    private Object get(MethodInvocation invocation) {
        CacheKey key = new CacheKey(invocation);
        CacheValue cacheValue = cache.get(key);
        if (cacheValue != null) {
            if (System.currentTimeMillis() > cacheValue.expiredTime) {// 缓存已过期
                cache.remove(key);
                cacheValue.value = null;
            }
        }
        return cacheValue == null ? null : cacheValue.value;
    }

    /**
     * 缓存数据
     * 
     * @param invocation
     * @param value
     * @param ttl
     */
    private void set(MethodInvocation invocation, Object value, long ttl) {
        long expiredTime = System.currentTimeMillis() + ttl;
        cache.put(new CacheKey(invocation, expiredTime), new CacheValue(value, expiredTime));
    }

    /**
     * 定时清除过期缓存，及时释放内存
     */
    private void clearExpiredCache() {
        if (cache.isEmpty()) {
            return;
        }
        int count = 0;
        for (CacheKey key : cache.keySet()) {
            if (key.expiredTime < System.currentTimeMillis()) {
                cache.remove(key);
                count++;
            }
        }
        logger.info("clear {} expired cache entries", count);
    }

    public void init() {
        scheduler = Executors.newSingleThreadScheduledExecutor();
        scheduler.scheduleAtFixedRate(new Runnable() {
            public void run() {
                clearExpiredCache();
            }
        }, CLEAR_EXPIRED_RATE, CLEAR_EXPIRED_RATE, TimeUnit.SECONDS);
    }

    public void destroy() {
        scheduler.shutdown();
    }

    private static class CacheKey {
        String methodName;
        Object[] arguments;
        long expiredTime;

        public CacheKey(MethodInvocation invocation) {
            this.methodName = invocation.getMethod().getName();
            this.arguments = invocation.getArguments();
        }

        public CacheKey(MethodInvocation invocation, long expiredTime) {
            this(invocation);
            this.expiredTime = expiredTime;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + Arrays.hashCode(arguments);
            result = prime * result + ((methodName == null) ? 0 : methodName.hashCode());
            return result;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            CacheKey other = (CacheKey) obj;
            if (!Arrays.equals(arguments, other.arguments))
                return false;
            if (methodName == null) {
                if (other.methodName != null)
                    return false;
            } else if (!methodName.equals(other.methodName))
                return false;
            return true;
        }
    }

    private static class CacheValue {
        Object value;
        long expiredTime;

        public CacheValue(Object value, long expiredTime) {
            this.value = value;
            this.expiredTime = expiredTime;
        }
    }
}
